/*
 * Copyright 2009-2010 Belmont Software Services
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.belmont.backup.gui;

import java.io.File;
import java.util.Vector;
import java.util.Date;
import java.net.*;
import java.awt.Color;
import java.awt.Font;
import javax.swing.*;
import java.util.*;
import java.io.*;
import java.sql.*;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeModel;
import com.belmont.backup.*;
import org.json.*;

/**
 * The main class of the application.
 */
public class GuiMain implements IBackupConstants, IEventObserver, Runnable {
    BackupApplication app;
    BNRGuiView view;
    Vector<String> backupAdminList;
    Vector<String> backupUserList;
    Vector<String> backupList;
    JFrame frame;
    Font defaultFont = new Font("Helvetica", Font.PLAIN, 12);
    java.util.Hashtable<String, Color> colors = new java.util.Hashtable<String, Color>();
    java.util.Hashtable<String, Font> fonts = new java.util.Hashtable<String, Font>();
    long amountToTransfer;
    long amountTransferred;
    BackupClient backup;
    RestoreClient restore;
    IBackupServer server;
    Thread actionThread;
    Thread statusThread;
    boolean backupMode = false;
    JSONObject versions = null;
    Vector<String> restoreDirectories = new Vector<String>();
    String restoreVersions[];
    String restorePath = null;
    String backupname = null;
    String company;
    String product;
    String logoURL;
    String clientName;
    String companyURL;

    public GuiMain(BackupApplication app) {
	this.app = app;
	this.backup = app.getBackup();
	this.restore = app.getRestore();
	this.server = app.getServer();
	backupList = new Vector<String>();
	backupAdminList = new Vector<String>();
	backupUserList = Utils.vectorFromString(app.getString(CONFIG_BACKUP_USER_DIRECTORIES,null), ';');

	if (backupUserList == null) {
	    backupUserList = new Vector<String>();
	}
	
	backup.addObserver(this, 0, 0);
	restore.addObserver(this, 0, 0);
	server.getConfig().addObserver(this, 0, 0);
	view = new BNRGuiView(this);
	refreshConfig();
    }

    void refreshConfig() {
	refreshConfig(server.getConfig());
    }

    synchronized void refreshConfig(Config cf) {
	if (cf == null) {
	    return;
	}

	company = cf.getCompany();
	product = cf.getProduct();
	logoURL = cf.getLogoURL();
	clientName = cf.getClientName();
	companyURL = cf.getCompanyURL();

	String bl[] = cf.getBackupDirectories();
	if (bl != null) {
	    backupAdminList.removeAllElements();
	    for (int i=0; i < bl.length; i++) {
		String d = Utils.expandVariables(bl[i], null, true);
		if (! backupAdminList.contains(d)) {
		    backupAdminList.addElement(d);
		}
	    }

	    backupList = (Vector<String>)backupAdminList.clone();
	    if (cf.getAllowBackupDirChange()) {
		// merge the user and admin items
		int l = backupUserList.size();
		for (int i=0; i < l; i++) {
		    String p = backupUserList.elementAt(i);
		    if (! backupList.contains(p)) {
			backupList.addElement(p);
		    }
		}
	    }
	}

	if (cf.getAllowBackupDirChange()) {
	    if (backupUserList != null && backupUserList.size() > 0) {
		backupList = (Vector<String>)backupUserList.clone();
	    }
	}

	if (cf.getRestoreDirectory() != null) {
	    view.dir.setText(Utils.expandVariables(cf.getRestoreDirectory(), null, true));
	}
	view.backupList.setListData(backupList);
	view.setDirChangeUI(cf);
	view.setFilters();
    }

    boolean connect() {
	try {
	    if (app.connect()) {
		refreshConfig();
		return true;
	    } else {
		return false;
	    }
	} finally {
	    view.setServerStatus();
	}
    }
	
    public void init() throws IOException {
	view = new BNRGuiView(this);
    }

    Vector<String> getBackupListData() {
	return backupList;
    }

    void resetView() {
	view.progressBar.setIndeterminate(false);
	view.amountToTransfer.setText("");
	view.amountTransferred.setText("");
	view.currentFile.setText("");
	view.filesToTransfer.setText("");
	view.filesTransferred.setText("");
	amountToTransfer = 0;
	amountTransferred = 0;
	setScheduleUI();
    }

    void setScheduleUI() {
	long tm = app.nextScheduledBackup();

	if (tm == 0) {
	    view.nextSched.setText("None scheduled");
	} else {
	    view.nextSched.setText(new Date(tm).toString());
	}
    }

    static class StatusThread implements Runnable {
	GuiMain main;
	int waitTime;

	StatusThread(GuiMain main, int waitTime) {
	    this.main = main;
	    this.waitTime = waitTime;
	}

	public void run() {
	    Status current = null;
	    Status scopy = new Status();
	    Status status = Status.getInstance();
	    long counter = status.getCounter();

	    while (true) {
		try {
		    Thread.sleep(waitTime);
		} catch (InterruptedException e) {
		    return;
		}
		status.copy(scopy);
		long c = status.getCounter();

		if (current != null && current.counter == scopy.counter) {
		    continue;
		}

		if (current == null || !current.currentOperation.equals(scopy.currentOperation)) {
		    main.view.currentOperation.setText(scopy.currentOperation);
		}
		if (current == null || !current.currentFile.equals(scopy.currentFile)) {
		    main.setCurrentFile(scopy.currentFile);
		}
		if (current == null || current.filesToTransfer != scopy.filesToTransfer) {
		    main.view.filesToTransfer.setText(Integer.toString(scopy.filesToTransfer));
		}
		if (current == null || current.filesTransferred != scopy.filesTransferred) {
		    main.view.filesTransferred.setText(Integer.toString(scopy.filesTransferred));
		}
		if (current == null || current.amountToTransfer != scopy.amountToTransfer) {
		    main.view.amountToTransfer.setText(Utils.formatSize(scopy.amountToTransfer));
		}

		if (current == null || current.amountTransferred != scopy.amountTransferred) {
		    int percentCompleted = 0;
		    percentCompleted = (int)(((float)scopy.amountTransferred / (float)scopy.amountToTransfer) * 100.0f);
		    main.view.amountTransferred.setText(Utils.formatSize(scopy.amountTransferred));
		    main.view.progressBar.setIndeterminate(false);
		    main.view.progressBar.setValue(percentCompleted);
		}
		//current = scopy;
	    }
	}
    }

    public void notify(Object sender, int msg, Object arg) {
	try {
	    if (view == null) {
		return;
	    }
	    switch (msg) {
	    case NOTIFY_CONFIG_CHANGED:
		setScheduleUI();
		// very important that refreshConfig not call any methods
		// that may send notifications as this could lead to deadlock
		// so we pass in a Config so it doesn't need to ask the
		// server for one.
		refreshConfig((Config)arg);
		break;
	    case NOTIFY_SCHEDULE_CHANGED:
		setScheduleUI();
		break;
	    case NOTIFY_BACKUP_START:
		backupMode = true;
		view.currentTask.setText("Backup "+arg);
		view.progressBar.setIndeterminate(true);
		statusThread = new Thread(new StatusThread(this, 150));
		statusThread.start();
		break;
	    case NOTIFY_RESTORE_START:
		backupMode = false;
		view.currentTask.setText("Restore "+arg);
		view.progressBar.setIndeterminate(true);
		statusThread = new Thread(new StatusThread(this, 150));
		statusThread.start();
		break;
	    case NOTIFY_BACKUP_FAILED:
	    case NOTIFY_PARTIAL_BACKUP:
	    case NOTIFY_RESTORE_FAILED:
		view.currentOperation.setText((backupMode) ? "Backup Stopped (failed)" : "Restore Stopped (failed)");
		view.resetStatus();
		if (statusThread != null) {
		    statusThread.interrupt();
		    statusThread = null;
		}
		break;
	    case NOTIFY_BACKUP_DONE:
	    case NOTIFY_RESTORE_DONE:
		view.currentOperation.setText((backupMode) ? "Backup Completed" : "Restore Completed");
		resetView();
		view.resetStatus();
		if (statusThread != null) {
		    statusThread.interrupt();
		    statusThread = null;
		}
		break;
	    case NOTIFY_FILE_COUNT_CHANGED:
		view.filesToTransfer.setText(Integer.toString(((Integer)arg).intValue()));
		break;
	    case NOTIFY_COMPLETED_FILE_COUNT_CHANGED:
		view.filesTransferred.setText(Integer.toString(((Integer)arg).intValue()));
		break;
	    case NOTIFY_SCANNING_START:
		view.currentOperation.setText("Scanning files");
		setCurrentFile((String)arg);
		break;
	    case NOTIFY_SCANNING_STOP:
		view.currentOperation.setText("");
		break;
	    case NOTIFY_ERROR_FILE:
	    case NOTIFY_FILE_ERROR:
		break;
	    case NOTIFY_EXCEPTION:
		Utils.log(LOG_ERROR, "Received exception", (Throwable)arg);
		break;
	    case NOTIFY_START_FILE:
		if (backupMode) {
		    view.currentOperation.setText("Backing up file");
		} else {
		    view.currentOperation.setText("Restoring file");
		}
		setCurrentFile((String)arg);
		break;
	    case NOTIFY_COMPLETED_FILE:
		view.currentFile.setText("");
		break;
	    case NOTIFY_TRANSFER_SIZE:
		amountToTransfer = ((Long)arg).longValue();
		view.amountToTransfer.setText(Utils.formatSize(amountToTransfer));
		break;
	    case NOTIFY_TRANSFER_COMPLETE:
		amountTransferred = amountToTransfer;
		// fall into next case
	    case NOTIFY_TRANSFER_PROGRESS:
		{
		    if (arg != null) {
			amountTransferred += ((Long)arg).longValue();
		    }
		    view.amountTransferred.setText(Utils.formatSize(amountTransferred));
		    view.progressBar.setIndeterminate(false);
		    int percentCompleted = 0;
		    if (amountTransferred > 0) {
			percentCompleted = (int)(((float)amountTransferred / (float)amountToTransfer) * 100.0f);
		    }
		    view.progressBar.setValue(percentCompleted);
		}
		break;
	    }
	} catch (Throwable e) {
	    e.printStackTrace();
	}
    }

    void setCurrentFile(String f) {
	if (f != null) {
	    int l = f.length();
	    int c = 50;
	    int c2 = 5;
	    if (l > c) {
		f = f.substring(0, c2)+"..."+f.substring((l - c), l);
	    }
	    view.currentFile.setText(f);
	}
    }

    boolean addBackupDirectory(File dir) {
	String path = dir.getAbsolutePath();

	if (backupUserList.contains(path)) {
	    return false;
	}
	backupUserList.addElement(path);
	backupList.addElement(path);
	app.setString(CONFIG_BACKUP_USER_DIRECTORIES, Utils.stringFromVector(backupUserList, ';'));

	return true;
    }

    boolean delBackupDirectories(Object dirs[]) {
	for (int i=0; i < dirs.length; i++) {
	    backupUserList.remove((String)dirs[i]);
	    backupList.remove((String)dirs[i]);
	}
	app.setString(CONFIG_BACKUP_USER_DIRECTORIES, Utils.stringFromVector(backupUserList, ';'));
	return true;
    }

    public java.awt.Color getColor(String key) {
	String v = app.getString(key);

	if (v == null) {
	    return Color.BLACK;
	}

	Color c = colors.get(v);

	if (c != null) {
	    return c;
	}

	int r;
	int g;
	int b;
	int p = 0;
	int sep = v.indexOf(',');
	try {
	    if (sep == -1) {
		return Color.BLACK;
	    }
	    r = Integer.parseInt(v.substring(p, sep).trim());
	    p = sep+1;
	    sep = v.indexOf(',', p);
	    if (sep == -1) {
		return Color.BLACK;
	    }
	    g = Integer.parseInt(v.substring(p, sep).trim());
	    b = Integer.parseInt(v.substring(sep+1).trim());

	    colors.put(v, c = new Color(r, g, b));
	    return c;
	} catch (NumberFormatException e) {
	    Utils.log(LOG_INFO, "Error in getColor", e);
	    e.printStackTrace();
	    return Color.BLACK;
	}
    }

    Font getFont(String key) {
	String v = app.getString(key);
	Font f = defaultFont;

	if (v == null) {
	    return f;
	}
	int sep = v.indexOf('-');
	String family;
	int style = Font.PLAIN;
	int size;

	if (sep == -1) {
	    return f;
	}
	family = v.substring(0, sep);
	v = v.substring(sep+1);
	try {
	    sep = v.indexOf('-');
	    if (sep == -1) {
		return f;
	    }

	    String st = v.substring(0, sep);
	    if ("bold".equalsIgnoreCase(st)) {
		style = Font.BOLD;
	    } else if ("italic".equalsIgnoreCase(st)) {
		style = Font.ITALIC;
	    }
	    size = Integer.parseInt(v.substring(sep+1).trim());
	    f = new Font(family, style, size);
	    fonts.put(v, f);
	    return f;
	} catch (NumberFormatException ex) {
	    Utils.log(LOG_ERROR, "Error in getFont", ex);
	    f = new Font(family, style, 12);
	    fonts.put(v, f);
	    return f;
	}
    }

    public ImageIcon createImageIcon(String path) {
	URL u = app.getBaseURL();
	try {
	    if (path.startsWith("http://")) {
		return new ImageIcon(new URL(path));
	    }

	    if (u == null) {
		return new ImageIcon(path);
	    }

	    URL imgURL = new URL(u, path);
	    if (imgURL != null) {
		return new ImageIcon(imgURL);
	    } else {
		Utils.log(LOG_ERROR, "Couldn't find file: " + path);
		return null;
	    }
	} catch (MalformedURLException e) {
	    Utils.log(LOG_ERROR, "Error in createImageIcon", e);
	    e.printStackTrace();
	    return null;
	}
    }

    public void stop() {
	app.stop();
    }

    public void start() {
	if (view == null) {
	    view = new BNRGuiView(this);
	}
	view.start();
	resetView();
	if (! connect()) {
	    view.print(app.getConnectStatus());
	}
    }

    public void show() {
	view.show();
    }

    public void hide() {
	view.hide();
    }

    void setStatusView() {
	if (backupMode) {
	    view.backupStartActionPerformed(null);
	} else {
	    view.restoreStartActionPerformed(null);
	}
    }

    boolean startBackup() {
	if (! connect()) {
	    view.print(app.getConnectStatus());
	    return false;
	}

	if (backupList.size() == 0) {
	    view.print("No directories to backup");
	    
	    return false;
	}

	if (actionThread != null) {
	    view.print("Client is busy");
	    return false;
	}

	actionThread = new Thread(this);
	backupMode = true;
	actionThread.start();

	return true;
    }

    void cancelBackup() {
	if (actionThread != null) {
	    Utils.log(LOG_INFO,"Cancelling backup");
	    Utils.requestCancel(null);
	}
    }

    boolean startRestore() {
	if (! connect()) {
	    view.print(app.getConnectStatus());
	    return false;
	}

	if (view.dir.getText().trim().length() == 0) {
	    view.print("Restore directory not set.");

	    return false;
	}

	if (actionThread != null) {
	    view.print("Client is busy");
	    return false;
	}

	app.setString(CONFIG_RESTORE_DIRECTORY, view.dir.getText().trim());

	actionThread = new Thread(this);
	backupMode = false;
	actionThread.start();

	return true;
    }

    Hashtable getSelectedFilter() {
	if (view.selectAll.isSelected()) {
	    return null;
	}

	Hashtable filter = new Hashtable();

	for (int i = 0; i < view.restoreTree.getRowCount(); i++) {
	    Object node = view.restoreTree.getPathForRow(i).getLastPathComponent();
	    if ((node != null) && (node instanceof DefaultMutableTreeNode))	{
		DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) node;
		Object userObject = treeNode.getUserObject();
		if ((treeNode.isLeaf()) && (userObject instanceof CheckBoxNode)) {
		    CheckBoxNode n = ((CheckBoxNode)userObject);
		    if (n.isSelected()) {
			Object v = filter.get(n.getChecksum());

			if (v == null) {
			    filter.put(n.getChecksum(), n.getFullPath());
			} else if (v instanceof String) {
			    Vector vs = new Vector();
			    vs.addElement(v);
			    vs.addElement(n.getFullPath());
			} else {
			    ((Vector)v).addElement(n.getFullPath());
			}
		    }
		}
	    }
	}

	return filter;
    }

    public void	getRestoreDirectories() {
	try {
	    if (! connect()) {
		view.print(app.getConnectStatus());
		return;
	    }

	    versions = server.getAvailableVersions(app.getSession());
	    if (versions != null) {
		Iterator k = versions.keys();
		while (k.hasNext()) {
		    String path = (String)k.next();
		    if (!restoreDirectories.contains(path)) {
			restoreDirectories.addElement(path);
		    }
		}
		view.restoreDirs.setListData(restoreDirectories);
		if (restoreDirectories.size() > 0) {
		    view.restoreDirs.setSelectedIndex(0);
		}
	    }
	} catch (JSONException ex) {
	    ex.printStackTrace();
	    view.print("Error getting directories from server.");
	    Utils.log(LOG_ERROR, "Error getting directories from server", ex);
	} catch (IOException ex) {
	    ex.printStackTrace();
	    view.print("Error getting directories from server.");
	    Utils.log(LOG_ERROR, "Error getting directories from server", ex);
	}
    }

    public void getRestoreVersions() {
	int idx = view.restoreDirs.getMinSelectionIndex();

	if (idx != -1) {
	    restorePath = restoreDirectories.elementAt(idx);
	    JSONArray v = versions.optJSONArray(restorePath);
	    if (v != null) {
		int l = v.length();
		Vector<String> rv = new Vector<String>();
		restoreVersions = new String[l];
		for (int i=0; i < l; i++) {
		    JSONObject ver = (JSONObject)v.opt(i);
		    if (BACKUP_STATUS_DONE.equals(ver.optString("status"))) {
			restoreVersions[i] = ver.optString("name");
			rv.addElement(new Date(ver.optLong("date")).toString());
		    }
		}
		view.restoreVersions.setListData(rv);
		if (l > 0) {
		    view.restoreVersions.setSelectedIndex(0);
		}
	    }
	}
    }

    public void getManifest() {
	int idx = view.restoreVersions.getMinSelectionIndex();
	if (idx == -1) {
	    return;
	}
	File f = new File(restorePath);
	backupname = restoreVersions[idx];
    }

    public void run() {
	if (backupMode) {
	    resetView();

	    int size = backupList.size();
	    for (int i=0; i < size; i++) {
		String path = backupList.elementAt(i);
		try {
		    view.currentOperation.setText("Backup Starting...");
		    backup.backup(new File(path), server);
		} catch (SQLException ex) {
		    view.print("Error reported for backup: "+ex.getMessage());
		    Utils.log(LOG_ERROR, "Error in backup", ex);
		} catch (InterruptedException ex) {
		    view.print("Backup operation cancelled.");
		    Utils.log(LOG_ERROR, "Backup cancelled", ex);
		} catch (IOException ex) {
		    view.print("Error reported for backup: "+ex.getMessage());
		    Utils.log(LOG_ERROR, "Error in backup", ex);
		}
		view.progressBar.setIndeterminate(false);
	    }
	} else {
	    resetView();
	    try {
		view.currentOperation.setText("Restore Starting...");
		restore.restore(backupname, restorePath, new File(view.dir.getText()), server, app.getSession(), getSelectedFilter());
	    } catch (SQLException ex) {
		view.print("Error reported for restore: "+ex.getMessage());
		Utils.log(LOG_ERROR, "Error in restore", ex);
	    } catch (JSONException ex) {
		view.print("Error reported for restore: "+ex.getMessage());		
		Utils.log(LOG_ERROR, "Error in restore", ex);
	    } catch (InterruptedException ex) {
		Utils.log(LOG_ERROR, "Restore cancelled", ex);
		view.print("Restore operation cancelled.");
	    } catch (IOException ex) {
		Utils.log(LOG_ERROR, "Error in restore", ex);
		view.print("Error reported for restore: "+ex.getMessage());
	    }
	    view.progressBar.setIndeterminate(false);
	}

	actionThread = null;
    }

    /**
     * Main method launching the application.
     */
    public static void main(String[] args) throws Exception {
	BackupApplication app = new BackupApplication(new File(args[0]), args[1].equals("network"));
	app.init();
	GuiMain main = new GuiMain(app);

	main.init();
	main.start();
	main.show();
	Thread.currentThread().join();
    }
}
